Cou-Cor#
Show imports
%load_ext autoreload
%autoreload 2
import itertools
import os
from functools import cache
from typing import List, Literal, Optional, Tuple
import ms3
import numpy as np
import pandas as pd
import plotly.express as px
from dimcat import Pipeline, plotting
import utils
pd.set_option("display.max_rows", 1000)
pd.set_option("display.max_columns", 500)
Show helpers
RESULTS_PATH = os.path.abspath(os.path.join(utils.OUTPUT_FOLDER, "couperin_study"))
os.makedirs(RESULTS_PATH, exist_ok=True)
def make_output_path(
filename: str,
extension=None,
path=RESULTS_PATH,
) -> str:
return utils.make_output_path(filename=filename, extension=extension, path=path)
def save_figure_as(
fig, filename, formats=("png", "pdf"), directory=RESULTS_PATH, **kwargs
):
if formats is not None:
for fmt in formats:
plotting.write_image(fig, filename, directory, format=fmt, **kwargs)
else:
plotting.write_image(fig, filename, directory, **kwargs)
def style_plotly(
fig,
save_as=None,
xaxes: Optional[dict] = None,
yaxes: Optional[dict] = None,
match_facet_yaxes=False,
**layout,
):
layout_args = dict(utils.STD_LAYOUT, **layout)
fig.update_layout(**layout_args)
xaxes_settings = dict(gridcolor="lightgrey")
if xaxes:
xaxes_settings.update(xaxes)
fig.update_xaxes(**xaxes_settings)
yaxes_settings = dict(gridcolor="lightgrey")
if yaxes:
yaxes_settings.update(yaxes)
fig.update_yaxes(**yaxes_settings)
if match_facet_yaxes:
for row_idx, row_figs in enumerate(fig._grid_ref):
for col_idx, col_fig in enumerate(row_figs):
fig.update_yaxes(
row=row_idx + 1,
col=col_idx + 1,
matches="y" + str(len(row_figs) * row_idx + 1),
)
if save_as:
save_figure_as(fig, save_as)
fig.show()
Loading data
Show source
D = utils.get_dataset("couperin_concerts", corpus_release="v2.2")
D_cor = utils.get_dataset("corelli", corpus_release="v2.7")
D
Dataset
=======
{'inputs': {'basepath': None,
'packages': {'couperin_concerts': ["'couperin_concerts.measures' (MuseScoreMeasures)",
"'couperin_concerts.notes' (MuseScoreNotes)",
"'couperin_concerts.expanded' (MuseScoreHarmonies)",
"'couperin_concerts.chords' (MuseScoreChords)",
"'couperin_concerts.metadata' (Metadata)"]}},
'outputs': {'basepath': None, 'packages': {}},
'pipeline': []}
Grouping data
Show source
pipeline = Pipeline(["KeySlicer", "ModeGrouper"])
grouped_D = D.apply_step(pipeline)
grouped_D_cor = D_cor.apply_step(pipeline)
grouped_D
SlicedGroupedDataset
====================
{'inputs': {'basepath': None,
'packages': {'couperin_concerts': ["'couperin_concerts.measures' (MuseScoreMeasures)",
"'couperin_concerts.notes' (MuseScoreNotes)",
"'couperin_concerts.expanded' (MuseScoreHarmonies)",
"'couperin_concerts.chords' (MuseScoreChords)",
"'couperin_concerts.metadata' (Metadata)"]}},
'outputs': {'basepath': None,
'packages': {'features': ["'couperin_concerts.expanded.keyannotations' (KeyAnnotations)"]}},
'pipeline': ['FeatureExtractor', 'KeySlicer', 'ModeGrouper']}
Starting point: DiMCAT’s BassNotes feature
Show source
bass_notes = D.apply_step(pipeline).get_feature("bassnotes")
bass_notes_cor = D_cor.apply_step(pipeline).get_feature("bassnotes")
bass_notes.df
| mc | mn | quarterbeats | duration_qb | mc_onset | mn_onset | timesig | staff | voice | volta | label | pedal | chord | numeral | form | figbass | changes | relativeroot | cadence | phraseend | chord_type | chord_tones | added_tones | root | alt_label | globalkey_is_minor | localkey_is_minor | globalkey_mode | localkey_mode | localkey_resolved | localkey_and_mode | root_roman | relativeroot_resolved | effective_localkey | effective_localkey_resolved | effective_localkey_is_minor | pedal_resolved | chord_and_mode | chord_reduced | chord_reduced_and_mode | applied_to_numeral | numeral_or_applied_to_numeral | intervals_over_bass | intervals_over_root | scale_degrees | scale_degrees_and_mode | scale_degrees_major | scale_degrees_minor | bass_degree | bass_degree_and_mode | bass_degree_major | bass_degree_minor | bass_note_over_local_tonic | globalkey | localkey | bass_note | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| mode | corpus | piece | localkey_slice | i | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| major | couperin_concerts | c01n01_prelude | [0.0, 16.0) | 0 | 1 | 0 | 0 | 2.00 | 0 | 1/2 | 4/4 | 1 | 1 | <NA> | G.I{ | <NA> | I | I | <NA> | <NA> | <NA> | <NA> | <NA> | { | M | (0, 4, 1) | () | 0 | <NA> | False | False | major | major | I | I, major | I | NaN | I | I | False | <NA> | I, major | I | I, major | <NA> | I | (M3, P5) | (M3, P5) | (1, 3, 5) | (1, 3, 5), major | (1, 3, 5) | (1, #3, 5) | 1 | 1, major | 1 | 1 | P1 | G | I | 0 |
| 1 | 2 | 1 | 2 | 2.00 | 0 | 0 | 4/4 | 1 | 1 | <NA> | V | <NA> | V | V | <NA> | <NA> | <NA> | <NA> | <NA> | <NA> | M | (1, 5, 2) | () | 1 | <NA> | False | False | major | major | I | I, major | V | NaN | I | I | False | <NA> | V, major | V | V, major | <NA> | V | (M3, P5) | (M3, P5) | (5, 7, 2) | (5, 7, 2), major | (5, 7, 2) | (5, #7, 2) | 5 | 5, major | 5 | 5 | P5 | G | I | 1 | ||||
| 2 | 2 | 1 | 4 | 0.50 | 1/2 | 1/2 | 4/4 | 1 | 1 | <NA> | I6 | <NA> | I6 | I | <NA> | 6 | <NA> | <NA> | <NA> | <NA> | M | (4, 1, 0) | () | 0 | <NA> | False | False | major | major | I | I, major | I | NaN | I | I | False | <NA> | I6, major | I6 | I6, major | <NA> | I | (m3, m6) | (M3, P5) | (3, 5, 1) | (3, 5, 1), major | (3, 5, 1) | (#3, 5, 1) | 3 | 3, major | 3 | #3 | M3 | G | I | 4 | ||||
| 3 | 2 | 1 | 9/2 | 0.50 | 5/8 | 5/8 | 4/4 | 1 | 1 | <NA> | I | <NA> | I | I | <NA> | <NA> | <NA> | <NA> | <NA> | <NA> | M | (0, 4, 1) | () | 0 | <NA> | False | False | major | major | I | I, major | I | NaN | I | I | False | <NA> | I, major | I | I, major | <NA> | I | (M3, P5) | (M3, P5) | (1, 3, 5) | (1, 3, 5), major | (1, 3, 5) | (1, #3, 5) | 1 | 1, major | 1 | 1 | P1 | G | I | 0 | ||||
| 4 | 2 | 1 | 5 | 0.75 | 3/4 | 3/4 | 4/4 | 1 | 1 | <NA> | V(4) | <NA> | V(4) | V | <NA> | <NA> | 4 | <NA> | <NA> | <NA> | M | (1, 0, 2) | () | 1 | <NA> | False | False | major | major | I | I, major | V | NaN | I | I | False | <NA> | V(4), major | V | V, major | <NA> | V | (P4, P5) | (P4, P5) | (5, 1, 2) | (5, 1, 2), major | (5, 1, 2) | (5, 1, 2) | 5 | 5, major | 5 | 5 | P5 | G | I | 1 | ||||
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| minor | couperin_concerts | parnasse_07 | [173.0, 212.0) | 230 | 52 | 52 | 411/2 | 0.25 | 3/8 | 3/8 | 4/4 | 1 | 1 | <NA> | i64 | <NA> | i64 | i | <NA> | 64 | <NA> | <NA> | <NA> | <NA> | m | (1, 0, -3) | () | 0 | <NA> | True | True | minor | minor | i | i, minor | i | NaN | i | i | True | <NA> | i64, minor | i64 | i64, minor | <NA> | i | (P4, m6) | (m3, P5) | (5, 1, 3) | (5, 1, 3), minor | (5, 1, b3) | (5, 1, 3) | 5 | 5, minor | 5 | 5 | P5 | b | i | 1 |
| 231 | 52 | 52 | 823/4 | 0.25 | 7/16 | 7/16 | 4/4 | 1 | 1 | <NA> | iio64 | <NA> | iio64 | ii | o | 64 | <NA> | <NA> | <NA> | <NA> | o | (-4, 2, -1) | () | 2 | <NA> | True | True | minor | minor | i | i, minor | ii | NaN | i | i | True | <NA> | iio64, minor | iio64 | iio64, minor | <NA> | ii | (a4, M6) | (m3, d5) | (6, 2, 4) | (6, 2, 4), minor | (b6, 2, 4) | (6, 2, 4) | 6 | 6, minor | b6 | 6 | m6 | b | i | -4 | ||||
| 232 | 52 | 52 | 206 | 1.00 | 1/2 | 1/2 | 4/4 | 1 | 1 | <NA> | i6 | <NA> | i6 | i | <NA> | 6 | <NA> | <NA> | <NA> | <NA> | m | (-3, 1, 0) | () | 0 | <NA> | True | True | minor | minor | i | i, minor | i | NaN | i | i | True | <NA> | i6, minor | i6 | i6, minor | <NA> | i | (M3, M6) | (m3, P5) | (3, 5, 1) | (3, 5, 1), minor | (b3, 5, 1) | (3, 5, 1) | 3 | 3, minor | b3 | 3 | m3 | b | i | -3 | ||||
| 233 | 52 | 52 | 207 | 1.00 | 3/4 | 3/4 | 4/4 | 1 | 1 | <NA> | V | <NA> | V | V | <NA> | <NA> | <NA> | <NA> | <NA> | <NA> | M | (1, 5, 2) | () | 1 | <NA> | True | True | minor | minor | i | i, minor | V | NaN | i | i | True | <NA> | V, minor | V | V, minor | <NA> | V | (M3, P5) | (M3, P5) | (5, #7, 2) | (5, #7, 2), minor | (5, 7, 2) | (5, #7, 2) | 5 | 5, minor | 5 | 5 | P5 | b | i | 1 | ||||
| 234 | 53 | 53 | 208 | 4.00 | 0 | 0 | 4/4 | 1 | 1 | <NA> | i|PAC} | <NA> | i | i | <NA> | <NA> | <NA> | <NA> | PAC | } | m | (0, -3, 1) | () | 0 | <NA> | True | True | minor | minor | i | i, minor | i | NaN | i | i | True | <NA> | i, minor | i | i, minor | <NA> | i | (m3, P5) | (m3, P5) | (1, 3, 5) | (1, 3, 5), minor | (1, b3, 5) | (1, 3, 5) | 1 | 1, minor | 1 | 1 | P1 | b | i | 0 |
8376 rows × 56 columns
If needed, the localkey_slice intervals can be resolved using this table:
Show source
local_keys = grouped_D.get_feature("KeyAnnotations")
utils.print_heading("Key Segments Couperin")
print(local_keys.groupby("mode").size().to_string())
local_keys_cor = grouped_D_cor.get_feature("KeyAnnotations")
utils.print_heading("\nKey Segments Corelli")
print(local_keys_cor.groupby("mode").size().to_string())
local_keys.head()
Key Segments Couperin
---------------------
mode
major 279
minor 287
Key Segments Corelli
---------------------
mode
major 345
minor 367
| mc | mn | quarterbeats | duration_qb | mc_onset | mn_onset | timesig | staff | voice | volta | label | globalkey_is_minor | localkey_is_minor | globalkey_mode | localkey_mode | localkey_resolved | localkey_and_mode | globalkey | localkey | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| mode | corpus | piece | localkey_slice | i | |||||||||||||||||||
| major | couperin_concerts | c01n01_prelude | [0.0, 16.0) | 0 | 1 | 0 | 0 | 16.0 | 0 | 1/2 | 4/4 | 1 | 1 | <NA> | G.I{ | False | False | major | major | I | I, major | G | I |
| [22.5, 32.0) | 22 | 7 | 6 | 45/2 | 9.5 | 1/8 | 1/8 | 4/4 | 1 | 1 | <NA> | V.V{ | False | False | major | major | V | V, major | G | V | |||
| [32.0, 40.0) | 35 | 9 | 8 | 32 | 8.0 | 1/2 | 1/2 | 4/4 | 1 | 1 | <NA> | IV.ii6{ | False | False | major | major | IV | IV, major | G | IV | |||
| [40.0, 47.0) | 41 | 11 | 9 | 40 | 7.0 | 1/2 | 1/2 | 4/4 | 1 | 1 | <NA> | V.V{ | False | False | major | major | V | V, major | G | V | |||
| [47.0, 98.0) | 48 | 13 | 11 | 47 | 51.0 | 1/4 | 1/4 | 4/4 | 1 | 1 | <NA> | I.V65 | False | False | major | major | I | I, major | G | I |
Show helpers
succession_map = dict(
ascending_major={
"1": "2",
"2": "3",
"3": "4",
"4": "5",
"5": "6",
"6": "7",
"7": "1",
},
ascending_minor={
"1": "2",
"2": "3",
"3": "4",
"4": "5",
"5": "#6",
"#6": "#7",
"#7": "1",
},
descending={"1": "7", "2": "1", "3": "2", "4": "3", "5": "4", "6": "5", "7": "6"},
)
def inverse_dict(d):
return {v: k for k, v in d.items()}
predecessor_map = dict(
ascending_major=inverse_dict(succession_map["ascending_major"]),
ascending_minor=inverse_dict(succession_map["ascending_minor"]),
descending=inverse_dict(succession_map["descending"]),
)
def make_precise_preceding_movement_column(df):
"""Expects a dataframe containing the columns bass_degree, preceding_bass_degree, and preceding_movement,"""
preceding_movement_precise = df.preceding_movement.where(
df.preceding_movement != "step", df.preceding_interval
)
expected_ascending_degree = pd.concat(
[
df.loc[["major"], "bass_degree"].map(predecessor_map["ascending_major"]),
df.loc[["minor"], "bass_degree"].map(predecessor_map["ascending_minor"]),
]
)
expected_descending_degree = df.bass_degree.map(predecessor_map["descending"])
preceding_movement_precise = preceding_movement_precise.where(
df.preceding_bass_degree != expected_ascending_degree, "ascending"
)
preceding_movement_precise = preceding_movement_precise.where(
df.preceding_bass_degree != expected_descending_degree, "descending"
)
return preceding_movement_precise
def make_precise_subsequent_movement_column(df):
"""Expects a dataframe containing the columns bass_degree, subsequent_bass_degree, and subsequent_movement,"""
subsequent_movement_precise = df.subsequent_movement.where(
df.subsequent_movement != "step", df.subsequent_interval
)
expected_ascending_degree = pd.concat(
[
df.loc[["major"], "bass_degree"].map(succession_map["ascending_major"]),
df.loc[["minor"], "bass_degree"].map(succession_map["ascending_minor"]),
]
)
expected_descending_degree = df.bass_degree.map(succession_map["descending"])
subsequent_movement_precise = subsequent_movement_precise.where(
df.subsequent_bass_degree != expected_ascending_degree, "ascending"
)
subsequent_movement_precise = subsequent_movement_precise.where(
df.subsequent_bass_degree != expected_descending_degree, "descending"
)
return subsequent_movement_precise
This is the main table of this notebook. It corresponds to the BassNotes features,
with a preceding_ and a subsequent_ copy of each column concatenated to the right.
The respective upward and downward shifts are performed within each localkey group,
leaving first bass degrees with undefined preceding values and last bass degrees without
undefined subsequent values.
Show source
def make_adjacency_table(bass_notes):
preceding = bass_notes.groupby(["piece", "localkey_slice"]).shift()
preceding.columns = "preceding_" + preceding.columns
subsequent = bass_notes.groupby(["piece", "localkey_slice"]).shift(-1)
subsequent.columns = "subsequent_" + subsequent.columns
BN = pd.concat([bass_notes, preceding, subsequent], axis=1)
BN["preceding_iv"] = BN.bass_note - BN.preceding_bass_note
BN["subsequent_iv"] = BN.subsequent_bass_note - BN.bass_note
BN["preceding_interval"] = ms3.transform(
BN.preceding_iv, ms3.fifths2iv, smallest=True
)
BN["subsequent_interval"] = ms3.transform(
BN.subsequent_iv, ms3.fifths2iv, smallest=True
)
BN["preceding_iv_is_step"] = BN.preceding_iv.isin(
(-5, -2, 2, 5)
).where( # +m2, -M2, +M2, -m2
BN.preceding_iv.notna()
)
BN["subsequent_iv_is_step"] = BN.subsequent_iv.isin((-5, -2, 2, 5)).where(
BN.subsequent_iv.notna()
)
BN["preceding_iv_is_0"] = BN.preceding_iv == 0
BN["subsequent_iv_is_0"] = BN.subsequent_iv == 0
BN["preceding_movement"] = (
BN.preceding_iv_is_step.map({True: "step", False: "leap"})
.where(~BN.preceding_iv_is_0, "same")
.where(BN.preceding_iv.notna(), "none")
)
BN["subsequent_movement"] = (
BN.subsequent_iv_is_step.map({True: "step", False: "leap"})
.where(~BN.subsequent_iv_is_0, "same")
.where(BN.subsequent_iv.notna(), "none")
)
BN["preceding_movement_precise"] = make_precise_preceding_movement_column(BN)
BN["subsequent_movement_precise"] = make_precise_subsequent_movement_column(BN)
return BN
BN = make_adjacency_table(bass_notes)
BN_cor = make_adjacency_table(bass_notes_cor)
Show source
ignore_mask = BN.subsequent_interval.isna() | BN.subsequent_interval.duplicated()
interval2fifths = ( # mapping that allows to order the x-axis with intervals according to LoF
BN.loc[~ignore_mask, ["subsequent_interval", "subsequent_iv"]]
.set_index("subsequent_interval")
.iloc[:, 0]
.sort_values()
)
Overview of how the bass moves#
Intervals#
Show source
def plot_bass_movement(BN, corpus_name):
interval_data = pd.concat(
[
BN.groupby("mode").subsequent_interval.value_counts(normalize=True),
BN.groupby(["piece", "mode"])
.subsequent_interval.value_counts(normalize=True)
.groupby(["mode", "subsequent_interval"])
.sem()
.rename("std_err"),
],
axis=1,
).reset_index()
fig = px.bar(
interval_data,
x="subsequent_interval",
y="proportion",
color="mode",
barmode="group",
error_y="std_err",
color_discrete_map=utils.MAJOR_MINOR_COLORS,
labels=dict(subsequent_interval="Interval"),
title=f"Mode-wise proportion of how often a bass note moves by an interval in {corpus_name}",
category_orders=dict(subsequent_interval=interval2fifths.index),
)
style_plotly(fig, f"how_often_a_bass_note_moves_by_an_interval_{corpus_name}")
plot_bass_movement(BN, "Couperin")
plot_bass_movement(BN_cor, "Corelli")
Types of movement#
The values ascending and descending designate stepwise movement within the regola. Only non-chromatic scale
degrees can have these values with the exception of #6 and #7 which are considered diatonic in the context of
this study.
Show source
def plot_movement_types(BN, corpus_name, precise_categories=True):
subsequent_movement = (
"subsequent_movement_precise" if precise_categories else "subsequent_movement"
)
movement_data = pd.concat(
[
BN.groupby("mode")[subsequent_movement].value_counts(
normalize=True, dropna=False
),
BN.groupby(["piece", "mode"])[subsequent_movement]
.value_counts(normalize=True, dropna=False)
.groupby(["mode", subsequent_movement])
.sem()
.rename("std_err"),
],
axis=1,
).reset_index()
movement_data[subsequent_movement] = movement_data[subsequent_movement].fillna(
"none"
)
fig = px.bar(
movement_data,
x=subsequent_movement,
y="proportion",
color="mode",
barmode="group",
error_y="std_err",
color_discrete_map=utils.MAJOR_MINOR_COLORS,
labels={subsequent_movement: "Movement"},
title=f"Mode-wise proportion of a bass note moving in a certain manner in {corpus_name}",
category_orders=dict(subsequent_interval=interval2fifths.index),
)
style_plotly(fig, save_as=f"mode-wise_bass_motion_{corpus_name}")
plot_movement_types(BN, "Couperin")
plot_movement_types(BN_cor, "Corelli")
Sankey diagrams showing movement types before and after each scale degree#
Show helpers
def make_sankey_data(
five_major, color_edges=True, precise=True
) -> Tuple[pd.DataFrame, List[str], List[str]] | Tuple[pd.DataFrame, List[str]]:
preceding_movement = (
"preceding_movement_precise" if precise else "preceding_movement"
)
subsequent_movement = (
"subsequent_movement_precise" if precise else "subsequent_movement"
)
type_counts = five_major["intervals_over_bass"].value_counts()
preceding_movement_counts = five_major[preceding_movement].value_counts()
subsequent_movement_counts = five_major[subsequent_movement].value_counts()
preceding_links = five_major.groupby(
[preceding_movement]
).intervals_over_bass.value_counts()
subsequent_links = five_major.groupby(
[subsequent_movement]
).intervals_over_bass.value_counts()
node_labels = []
label_ids = dict()
for key, node_sizes in (
("preceding", preceding_movement_counts),
("intervals", type_counts),
("subsequent", subsequent_movement_counts),
):
for label in node_sizes.index:
label_id = len(node_labels)
node_labels.append(str(label))
label_ids[(key, label)] = label_id
edge_columns = ["source", "target", "value"]
if color_edges:
node_colors = utils.make_evenly_distributed_color_map(node_labels)
edge_columns.append("color")
links = []
for (prec_mov, iv), cnt in preceding_links.items():
source_id = label_ids.get(("preceding", prec_mov))
target_id = label_ids.get(("intervals", iv))
if color_edges:
edge_color = node_colors[source_id]
links.append((source_id, target_id, cnt, edge_color))
else:
links.append((source_id, target_id, cnt))
for (subs_mov, iv), cnt in subsequent_links.items():
source_id = label_ids.get(("intervals", iv))
target_id = label_ids.get(("subsequent", subs_mov))
if color_edges:
edge_color = node_colors[target_id]
links.append((source_id, target_id, cnt, edge_color))
else:
links.append((source_id, target_id, cnt))
edge_data = pd.DataFrame(links, columns=edge_columns)
if color_edges:
return edge_data, node_labels, node_colors
return edge_data, node_labels
def make_bass_degree_sankey(
BN: pd.DataFrame,
corpus: str,
mode: Literal["major", "minor"],
bass_degree: Optional[str | int] = None,
**layout,
):
"""bass_degree None means all unigrams."""
selected_unigrams = BN.loc[mode]
if bass_degree:
selected_unigrams = selected_unigrams.query(f"bass_degree == '{bass_degree}'")
selection_text = f"bass degree {bass_degree}"
else:
selection_text = "any harmony"
edge_data, node_labels, node_colors = make_sankey_data(selected_unigrams)
title = f"Motions to and from {selection_text} in {corpus} ({mode})"
fig = utils.make_sankey(
edge_data, node_labels, node_color=node_colors, title=title, **layout
)
return fig
All unigrams#
Major#
make_bass_degree_sankey(BN, "Couperin", "major")
make_bass_degree_sankey(BN, "Corelli", "major")
Minor#
make_bass_degree_sankey(BN, "Couperin", "minor")
Intervals over bass degree 1#
Major#
Show source
make_bass_degree_sankey(BN, "Couperin", "major", 1)
make_bass_degree_sankey(BN_cor, "Corelli", "major", 1)
Minor#
Show source
make_bass_degree_sankey(BN, "Couperin", "minor", 1)
make_bass_degree_sankey(BN_cor, "Corelli", "minor", 1)
make_bass_degree_sankey(BN, "Corelli", "minor")
Intervals over bass degree 2#
Major#
make_bass_degree_sankey(BN, "Couperin", "major", 2)
make_bass_degree_sankey(BN_cor, "Corelli", "major", 2)
Minor#
make_bass_degree_sankey(BN, "Couperin", "minor", 2)
make_bass_degree_sankey(BN_cor, "Corelli", "minor", 2)
Intervals over bass degree 3#
Major#
make_bass_degree_sankey(BN, "Couperin", "major", 3)
make_bass_degree_sankey(BN_cor, "Corelli", "major", 3)
Minor#
make_bass_degree_sankey(BN, "Couperin", "minor", 3)
make_bass_degree_sankey(BN_cor, "Corelli", "minor", 3)
Intervals over bass degree 4#
Major#
make_bass_degree_sankey(BN, "Couperin", "major", 4)
make_bass_degree_sankey(BN_cor, "Corelli", "major", 4)
Minor#
make_bass_degree_sankey(BN, "Couperin", "minor", 4)
make_bass_degree_sankey(BN_cor, "Corelli", "minor", 4)
Intervals over bass degree 5#
Major#
make_bass_degree_sankey(BN, "Couperin", "major", 5)
make_bass_degree_sankey(BN_cor, "Corelli", "major", 5)
Minor#
make_bass_degree_sankey(BN, "Couperin", "minor", 5)
make_bass_degree_sankey(BN_cor, "Corelli", "minor", 5)
Intervals over bass degree 6#
Major#
make_bass_degree_sankey(BN, "Couperin", "major", 6)
make_bass_degree_sankey(BN_cor, "Corelli", "major", 6)
Minor (ascending)#
make_bass_degree_sankey(BN, "Couperin", "minor", "#6")
make_bass_degree_sankey(BN, "Corelli", "minor", "#6")
Minor (descending)#
make_bass_degree_sankey(BN, "Couperin", "minor", 6)
make_bass_degree_sankey(BN_cor, "Corelli", "minor", 6)
Intervals over bass degree 7#
Major#
make_bass_degree_sankey(BN, "Couperin", "major", 7)
make_bass_degree_sankey(BN_cor, "Corelli", "major", 7)
Minor (ascending)#
make_bass_degree_sankey(BN, "Couperin", "minor", "#7")
make_bass_degree_sankey(BN, "Corelli", "minor", "#7")
Minor (descending)#
make_bass_degree_sankey(BN, "Couperin", "minor", 7)
make_bass_degree_sankey(BN_cor, "Corelli", "minor", 7)
Explanatory power of the RoO#
Defining the vocabulary#
maj = ("M3", "P5")
maj6 = ("m3", "m6")
min = ("m3", "P5")
min6 = ("M3", "M6")
Mm56 = ("m3", "d5", "m6")
Mm34 = ("m3", "P4", "M6")
Mm24 = ("M2", "a4", "M6")
mm56 = ("M3", "P5", "M6")
hdim56 = ("m3", "P5", "M6")
hdim34 = ("M3", "a4", "M6")
regole = dict(
ascending_major=[
("1", maj), # most frequent
("2", Mm34), # most frequent
("3", maj6), # most frequent
("4", mm56), # not most frequent
("5", maj), # most frequent
("6", maj6), # not most frequent
("7", Mm56), # most frequent
],
descending_major=[
("1", maj), # same
("7", maj6), # different, not most frequent
("6", Mm34), # different, not most frequent either
("5", maj), # same
("4", Mm24), # different, not most frequent either
("3", maj6), # same
("2", Mm34), # same
],
ascending_minor=[
("1", min), # most frequent
("2", Mm34), # most frequent
("3", min6), # most frequent
("4", hdim56), # most frequent
("5", maj), # most frequent
("#6", maj6), # most frequent
("#7", Mm56), # most frequent
],
descending_minor=[
("1", min), # same
("7", min6), # different, most frequent
("6", hdim34), # different, most frequent
("5", maj), # same
("4", Mm24), # different, not most frequent
("3", min6), # same
("2", Mm34), # same
],
)
regola_vocabulary_major = tuple(
set(regole["ascending_major"] + regole["descending_major"])
)
regola_vocabulary_minor = tuple(
set(regole["ascending_minor"] + regole["descending_minor"])
)
Most frequent chords for each bass degree#
Show source
def summarize_groups_top_k_chords(df, column="intervals_over_bass", k=3):
"""Used in Groupby.apply()"""
proportions = df[column].value_counts(normalize=True)
entropy = -(proportions * np.log2(proportions)).sum()
N = len(proportions)
normalized_entropy = entropy / np.log2(N) if N > 1 else 0.0
top_k = proportions.iloc[:k]
rank_col = list(range(1, len(top_k) + 1))
result = pd.DataFrame(
dict(
intervals_over_bass=top_k.index,
proportion=top_k.values,
normalized_entropy=normalized_entropy,
),
index=rank_col,
).rename_axis("rank_chord")
return result
def rank_bass_degrees(df: pd.Series):
"""Used in Groupby.apply()"""
vc = df.bass_degree.value_counts(normalize=True).to_frame()
vc["rank_bass"] = list(range(1, len(vc) + 1))
return vc
def summarize_degree_wise_top_k(BN, column="intervals_over_bass", k=3):
result = (
BN.groupby(["mode", "bass_degree"]).apply(
summarize_groups_top_k_chords, column=column, k=k
)
).reset_index(level=-1)
bass_proportions = BN.groupby("mode").apply(rank_bass_degrees)
result = result.join(bass_proportions, lsuffix="_chord", rsuffix="_bass")
return result
def degree_wise_top_k(BN, column="intervals_over_bass", k=3):
summary = summarize_degree_wise_top_k(BN, column=column, k=k)
result = []
for mode, df in summary.groupby("mode"):
vocab = regola_vocabulary_major if mode == "major" else regola_vocabulary_minor
is_regola = (
df.reset_index(level="bass_degree")[["bass_degree", "intervals_over_bass"]]
.apply(tuple, axis=1)
.isin(vocab)
).values
df["is_regola"] = is_regola
df = (
df.sort_values(["rank_bass", "rank_chord"])
.reset_index("bass_degree")
.reset_index("mode", drop=True)
.set_index(
[
"rank_bass",
"bass_degree",
"proportion_bass",
"normalized_entropy",
"rank_chord",
]
)
)[["intervals_over_bass", "proportion_chord", "is_regola"]]
result.append(df)
return result
def style_rank_table(df: pd.DataFrame):
def color_true_green(value):
if value:
return "background-color: lightgreen"
return None
new_index_names = dict(
rank_bass="Rank",
bass_degree="Bass Degree",
proportion_bass="Proportion",
normalized_entropy="Entropy",
rank_chord="Top",
)
df = df.rename_axis(index=new_index_names)
return (
df.style.format({"proportion_chord": "{:.1%}"})
.format_index(
axis=0,
formatter={
"Proportion": "{:.1%}", # Format as percentage with 1 decimal place
"Entropy": "{:.3f}", # Format as float with 3 decimal places
},
)
.map(color_true_green, subset=["is_regola"])
.relabel_index(["Chord", "Proportion", "Regola"], axis=1)
# .format_index_names(new_index_names, axis=0) # available in a future pandas version
# https://pandas.pydata.org/docs/dev/reference/api/pandas.io.formats.style.Styler.format_index_names.html
)
Major#
Couperin#
Show source
major, minor = degree_wise_top_k(BN)
style_rank_table(major)
| Chord | Proportion | Regola | |||||
|---|---|---|---|---|---|---|---|
| Rank | Bass Degree | Proportion | Entropy | Top | |||
| 1 | 5 | 24.9% | 0.488 | 1 | ('M3', 'P5') | 59.7% | True |
| 2 | ('M3', 'P5', 'm7') | 22.1% | False | ||||
| 3 | ('P4', 'P5') | 7.9% | False | ||||
| 2 | 1 | 24.7% | 0.219 | 1 | ('M3', 'P5') | 89.4% | True |
| 2 | ('M3', 'M6') | 2.7% | False | ||||
| 3 | ('M3', 'P5', 'm7') | 2.0% | False | ||||
| 3 | 4 | 12.1% | 0.622 | 1 | ('M3', 'P5') | 34.3% | False |
| 2 | ('M3', 'P5', 'M6') | 31.7% | True | ||||
| 3 | ('M2', 'a4', 'M6') | 15.8% | True | ||||
| 4 | 3 | 11.6% | 0.271 | 1 | ('m3', 'm6') | 86.5% | True |
| 2 | ('m3', 'P5') | 3.1% | False | ||||
| 3 | ('m3', 'd5', 'm6') | 2.9% | False | ||||
| 5 | 7 | 9.0% | 0.402 | 1 | ('m3', 'd5', 'm6') | 62.6% | True |
| 2 | ('m3', 'm6') | 31.5% | True | ||||
| 3 | ('m3', 'd5', 'm7') | 2.4% | False | ||||
| 6 | 6 | 8.1% | 0.659 | 1 | ('m3', 'P5') | 36.5% | False |
| 2 | ('m3', 'm6') | 24.9% | True | ||||
| 3 | ('m3', 'P5', 'm7') | 10.8% | False | ||||
| 7 | 2 | 8.0% | 0.691 | 1 | ('m3', 'P4', 'M6') | 39.5% | True |
| 2 | ('m3', 'P5') | 27.7% | False | ||||
| 3 | ('m3', 'P5', 'm7') | 16.0% | False | ||||
| 8 | #4 | 0.8% | 0.722 | 1 | ('m3', 'd5', 'm6') | 80.0% | False |
| 2 | ('m3', 'm6') | 20.0% | False | ||||
| 9 | b7 | 0.6% | 0.847 | 1 | ('M2', 'a4', 'M6') | 39.1% | False |
| 2 | ('M3', 'M6') | 39.1% | False | ||||
| 3 | ('M3', 'a4', 'M6') | 17.4% | False | ||||
| 10 | #1 | 0.1% | 0.000 | 1 | ('m3', 'd5', 'm6') | 100.0% | False |
| 11 | b3 | 0.0% | 1.000 | 1 | ('M3', 'a5', 'M7') | 50.0% | False |
| 2 | ('M3', 'M6') | 50.0% | False | ||||
| 12 | #5 | 0.0% | 1.000 | 1 | ('m3', 'd5', 'd7') | 50.0% | False |
| 2 | ('m3', 'm6') | 50.0% | False |
Corelli#
major_cor, minor_cor = degree_wise_top_k(BN_cor)
style_rank_table(major_cor)
| Chord | Proportion | Regola | |||||
|---|---|---|---|---|---|---|---|
| Rank | Bass Degree | Proportion | Entropy | Top | |||
| 1 | 5 | 23.7% | 0.501 | 1 | ('M3', 'P5') | 51.8% | True |
| 2 | ('M3', 'P5', 'm7') | 16.9% | False | ||||
| 3 | ('P4', 'P5') | 14.7% | False | ||||
| 2 | 1 | 20.9% | 0.297 | 1 | ('M3', 'P5') | 80.8% | True |
| 2 | ('M3', 'M6') | 7.7% | False | ||||
| 3 | ('P4', 'P5') | 4.2% | False | ||||
| 3 | 4 | 15.5% | 0.577 | 1 | ('M3', 'P5') | 43.0% | False |
| 2 | ('M3', 'M6') | 23.0% | False | ||||
| 3 | ('M3', 'P5', 'M6') | 16.8% | True | ||||
| 4 | 3 | 12.2% | 0.414 | 1 | ('m3', 'm6') | 70.9% | True |
| 2 | ('m3', 'P5') | 11.4% | False | ||||
| 3 | ('m3', 'm7') | 4.1% | False | ||||
| 5 | 6 | 10.7% | 0.562 | 1 | ('m3', 'P5') | 42.4% | False |
| 2 | ('m3', 'm6') | 27.9% | True | ||||
| 3 | ('m3', 'P5', 'm7') | 8.9% | False | ||||
| 6 | 2 | 8.1% | 0.703 | 1 | ('m3', 'P5') | 28.3% | False |
| 2 | ('m3', 'M6') | 20.2% | False | ||||
| 3 | ('m3', 'P5', 'm7') | 16.4% | False | ||||
| 7 | 7 | 7.2% | 0.540 | 1 | ('m3', 'm6') | 54.8% | True |
| 2 | ('m3', 'd5') | 17.0% | False | ||||
| 3 | ('m3', 'd5', 'm6') | 14.1% | True | ||||
| 8 | #4 | 0.9% | 0.743 | 1 | ('m3', 'm6') | 50.7% | False |
| 2 | ('m3', 'd5', 'm6') | 21.7% | False | ||||
| 3 | ('m3', 'd5', 'd7') | 15.9% | False | ||||
| 9 | #5 | 0.4% | 0.861 | 1 | ('m3', 'm6') | 40.0% | False |
| 2 | ('m3', 'd5', 'm6') | 30.0% | False | ||||
| 3 | ('m3', 'd5') | 26.7% | False | ||||
| 10 | #1 | 0.1% | 0.966 | 1 | ('m3', 'd5', 'm6') | 44.4% | False |
| 2 | ('m3', 'm6') | 33.3% | False | ||||
| 3 | ('m3', 'd5') | 22.2% | False | ||||
| 11 | b6 | 0.1% | 0.982 | 1 | ('M3', 'M6') | 42.9% | False |
| 2 | ('M3', 'P5') | 28.6% | False | ||||
| 3 | ('M3', 'M7') | 28.6% | False | ||||
| 12 | b7 | 0.1% | 0.961 | 1 | ('M3', 'P5') | 40.0% | False |
| 2 | ('M3', 'm7') | 20.0% | False | ||||
| 3 | ('M3', 'M6') | 20.0% | False | ||||
| 13 | b3 | 0.1% | 0.000 | 1 | ('M3', 'M6') | 100.0% | False |
| 14 | #2 | 0.0% | 0.000 | 1 | ('m3', 'm6') | 100.0% | False |
| 15 | b2 | 0.0% | 0.000 | 1 | ('M3', 'M6') | 100.0% | False |
Minor#
Couperin#
style_rank_table(minor)
| Chord | Proportion | Regola | |||||
|---|---|---|---|---|---|---|---|
| Rank | Bass Degree | Proportion | Entropy | Top | |||
| 1 | 5 | 23.3% | 0.533 | 1 | ('M3', 'P5') | 56.8% | True |
| 2 | ('M3', 'P5', 'm7') | 18.8% | False | ||||
| 3 | ('P4', 'm6') | 9.9% | False | ||||
| 2 | 1 | 22.2% | 0.280 | 1 | ('m3', 'P5') | 81.8% | True |
| 2 | ('M3', 'P5') | 8.8% | False | ||||
| 3 | ('P4', 'P5') | 1.6% | False | ||||
| 3 | 4 | 14.9% | 0.654 | 1 | ('m3', 'P5', 'M6') | 36.6% | True |
| 2 | ('m3', 'P5') | 19.3% | False | ||||
| 3 | ('M2', 'a4', 'M6') | 15.5% | True | ||||
| 4 | 3 | 11.6% | 0.338 | 1 | ('M3', 'M6') | 80.7% | True |
| 2 | ('M3', 'P5') | 7.5% | False | ||||
| 3 | ('M3', 'a5', 'M7') | 3.1% | False | ||||
| 5 | 2 | 8.0% | 0.667 | 1 | ('m3', 'P4', 'M6') | 50.7% | True |
| 2 | ('m3', 'd5', 'm7') | 10.9% | False | ||||
| 3 | ('m3', 'd5') | 10.0% | False | ||||
| 6 | 6 | 7.5% | 0.637 | 1 | ('M3', 'a4', 'M6') | 40.3% | True |
| 2 | ('M3', 'M6') | 21.7% | False | ||||
| 3 | ('M3', 'P5') | 17.9% | False | ||||
| 7 | #7 | 6.8% | 0.362 | 1 | ('m3', 'd5', 'm6') | 82.9% | True |
| 2 | ('m3', 'm6') | 12.9% | False | ||||
| 3 | ('m3', 'd5', 'd7') | 3.1% | False | ||||
| 8 | 7 | 2.7% | 0.780 | 1 | ('M3', 'M6') | 37.1% | True |
| 2 | ('M3', 'P5') | 25.0% | False | ||||
| 3 | ('M3', 'P5', 'm7') | 12.9% | False | ||||
| 9 | #3 | 1.6% | 0.528 | 1 | ('m3', 'd5', 'm6') | 73.1% | False |
| 2 | ('m3', 'm6') | 22.4% | False | ||||
| 3 | ('m3', 'd5') | 3.0% | False | ||||
| 10 | #6 | 1.2% | 0.717 | 1 | ('m3', 'm6') | 38.0% | True |
| 2 | ('m3', 'd5', 'm7') | 34.0% | False | ||||
| 3 | ('m3', 'd5', 'm6') | 14.0% | False | ||||
| 11 | #4 | 0.2% | 0.544 | 1 | ('m3', 'd5', 'm6') | 87.5% | False |
| 2 | ('m3', 'd5', 'd7') | 12.5% | False |
Corelli#
style_rank_table(minor_cor)
| Chord | Proportion | Regola | |||||
|---|---|---|---|---|---|---|---|
| Rank | Bass Degree | Proportion | Entropy | Top | |||
| 1 | 5 | 25.4% | 0.485 | 1 | ('M3', 'P5') | 52.2% | True |
| 2 | ('P4', 'P5') | 16.3% | False | ||||
| 3 | ('M3', 'P5', 'm7') | 14.1% | False | ||||
| 2 | 1 | 23.0% | 0.295 | 1 | ('m3', 'P5') | 80.8% | True |
| 2 | ('m3', 'm6') | 5.4% | False | ||||
| 3 | ('M3', 'P5') | 3.8% | False | ||||
| 3 | 4 | 15.0% | 0.638 | 1 | ('m3', 'P5') | 37.1% | False |
| 2 | ('m3', 'P5', 'M6') | 15.5% | True | ||||
| 3 | ('m3', 'M6') | 12.6% | False | ||||
| 4 | 6 | 9.5% | 0.538 | 1 | ('M3', 'M6') | 44.0% | False |
| 2 | ('M3', 'P5') | 26.6% | False | ||||
| 3 | ('M3', 'M7') | 18.8% | False | ||||
| 5 | 3 | 9.4% | 0.365 | 1 | ('M3', 'M6') | 69.9% | True |
| 2 | ('M3', 'P5') | 22.6% | False | ||||
| 3 | ('M3', 'M7') | 2.3% | False | ||||
| 6 | #7 | 5.4% | 0.666 | 1 | ('m3', 'm6') | 52.1% | False |
| 2 | ('m3', 'd5') | 22.5% | False | ||||
| 3 | ('m3', 'd5', 'm6') | 20.2% | True | ||||
| 7 | 7 | 4.7% | 0.573 | 1 | ('M3', 'M6') | 49.5% | True |
| 2 | ('M3', 'P5') | 19.6% | False | ||||
| 3 | ('M3', 'm7') | 9.0% | False | ||||
| 8 | 2 | 4.5% | 0.812 | 1 | ('m3', 'M6') | 25.5% | False |
| 2 | ('m3', 'd5', 'm7') | 16.2% | False | ||||
| 3 | ('m3', 'm6') | 12.1% | False | ||||
| 9 | #6 | 1.5% | 0.720 | 1 | ('m3', 'm6') | 35.1% | True |
| 2 | ('m3', 'm7') | 32.0% | False | ||||
| 3 | ('m3', 'd5', 'm6') | 27.8% | False | ||||
| 10 | #3 | 1.1% | 0.937 | 1 | ('m3', 'm6') | 50.7% | False |
| 2 | ('m3', 'd5', 'm6') | 28.2% | False | ||||
| 3 | ('m3', 'd5') | 21.1% | False | ||||
| 11 | #4 | 0.3% | 0.874 | 1 | ('m3', 'd5', 'd7') | 44.4% | False |
| 2 | ('m3', 'm6') | 27.8% | False | ||||
| 3 | ('m3', 'd5', 'm6') | 22.2% | False | ||||
| 12 | b1 | 0.1% | 0.960 | 1 | ('M3', 'P5', 'M6') | 40.0% | False |
| 2 | ('M3', 'M6') | 40.0% | False | ||||
| 3 | ('M3', 'M7') | 20.0% | False | ||||
| 13 | b2 | 0.1% | 0.961 | 1 | ('m3', 'M6') | 40.0% | False |
| 2 | ('M3', 'M7') | 20.0% | False | ||||
| 3 | ('M3', 'M6') | 20.0% | False | ||||
| 14 | b7 | 0.0% | 0.000 | 1 | ('a4', 'M6') | 100.0% | False |
| 15 | b5 | 0.0% | 0.000 | 1 | ('M3', 'M6') | 100.0% | False |
Show helpers
name2BN = {"couperin": BN, "corelli": BN_cor}
@cache
def get_base_df(
bn_name: str,
basis: Literal["major_all", "minor_all", "major_diatonic", "minor_diatonic"],
query: Optional[str] = None,
):
BN = name2BN[bn_name]
try:
mode, selection = basis.split("_")
except Exception:
raise ValueError(f"Invalid keyword for basis: {basis!r}")
base = BN.loc[[mode]]
if selection == "all":
result = base
elif selection == "diatonic":
if mode == "major":
result = base.query("bass_degree in ('1', '2', '3', '4', '5', '6', '7')")
elif mode == "minor":
result = base.query(
"bass_degree in ('1', '2', '3', '4', '5', '6', '#6', '7', '#7')"
)
else:
raise ValueError(f"Unknown keyword for selection: {selection!r}")
if query:
result = result.query(query)
return result
@cache
def get_bass_degree_mask(
bn_name: str,
basis: Literal["major_all", "minor_all", "major_diatonic", "minor_diatonic"],
bass_degree: str,
query: Optional[str] = None,
):
base = get_base_df(bn_name, basis, query=query)
return base.bass_degree == bass_degree
@cache
def get_intervals_mask(
bn_name,
basis: Literal["major_all", "minor_all", "major_diatonic", "minor_diatonic"],
intervals: tuple,
query: Optional[str] = None,
):
base = get_base_df(bn_name, basis, query=query)
return base.intervals_over_bass == intervals
@cache
def get_chord_mask(
bn_name,
basis: Literal["major_all", "minor_all", "major_diatonic", "minor_diatonic"],
bass_degree: str,
intervals: tuple,
query: Optional[str] = None,
):
bass_degree_mask = get_bass_degree_mask(
bn_name, basis=basis, bass_degree=bass_degree, query=query
)
intervals_mask = get_intervals_mask(
bn_name, basis=basis, intervals=intervals, query=query
)
return bass_degree_mask & intervals_mask
@cache
def get_chord_vocabulary_mask(
bn_name,
basis: Literal["major_all", "minor_all", "major_diatonic", "minor_diatonic"],
vocabulary: Tuple[Tuple[str, tuple], ...],
query: Optional[str] = None,
) -> pd.Series:
base = get_base_df(bn_name, basis, query=query)
mask = pd.Series(False, index=base.index, dtype="boolean")
for bass_degree, intervals in vocabulary:
mask |= get_chord_mask(
bn_name,
basis=basis,
bass_degree=bass_degree,
intervals=intervals,
query=query,
)
return mask
def inspect(
bn_name,
basis: Literal["major_all", "minor_all", "major_diatonic", "minor_diatonic"],
vocabulary: Tuple[Tuple[str, tuple], ...],
query: Optional[str] = None,
) -> pd.DataFrame:
base = get_base_df(bn_name, basis, query=query)
mask = get_chord_vocabulary_mask(
bn_name, basis=basis, vocabulary=vocabulary, query=query
)
return base[mask]
def get_vocabulary_coverage(
bn_name,
basis: Literal["major_all", "minor_all", "major_diatonic", "minor_diatonic"],
vocabulary: Tuple[Tuple[str, tuple], ...],
query: Optional[str] = None,
) -> float:
mask = get_chord_vocabulary_mask(
bn_name, basis=basis, vocabulary=vocabulary, query=query
)
return mask.sum() / len(mask)
def get_coverage_values(
bn_name,
major_vocabulary: Optional[Tuple[Tuple[str, tuple], ...]] = None,
minor_vocabulary: Optional[Tuple[Tuple[str, tuple], ...]] = None,
**name2query,
) -> pd.Series:
if not (major_vocabulary or minor_vocabulary):
return pd.Series()
results = {}
if major_vocabulary:
results.update(
{
("major", "all"): get_vocabulary_coverage(
bn_name, "major_all", major_vocabulary
),
("major", "diatonic"): get_vocabulary_coverage(
bn_name, "major_diatonic", major_vocabulary
),
}
)
for name, query in name2query.items():
results[("major", name)] = get_vocabulary_coverage(
bn_name, "major_diatonic", major_vocabulary, query=query
)
if minor_vocabulary:
results.update(
{
("minor", "all"): get_vocabulary_coverage(
bn_name, "minor_all", minor_vocabulary
),
("minor", "diatonic"): get_vocabulary_coverage(
bn_name, "minor_diatonic", minor_vocabulary
),
}
)
for name, query in name2query.items():
results[("minor", name)] = get_vocabulary_coverage(
bn_name, "minor_diatonic", minor_vocabulary, query=query
)
result = pd.Series(results, name="proportion")
result.index.names = ["mode", "coverage_of"]
return result
Which proportion of unigrams are “explained” by Campion’s regola#
The percentages are based on different sets of unigrams.
from means before/leading to a bass degree, to means after/following a bass degree.
all: all bass degreesdiatonic: all non-chromatic bass degrees (in minor, the chromatic scale degrees#6and#7are considered diatonic)to_ascending: all diatonic bass degrees that ascend within the regolafrom_ascending: all diatonic bass degrees that are reached by ascending within the regolato_and_from_ascending: all diatonic bass degrees that are reached by ascending within the regola and proceed ascending within the regolato_and_from_either: all diatonic bass degrees whose predecessor and successor are both upper or lower neighbors within the regolato_leap: all diatonic bass degrees followed by a leapto_same: all diatonic bass degrees followed by the same bass degreeetc.
Show source
features = dict(
to_ascending="subsequent_movement_precise == 'ascending'",
to_descending="subsequent_movement_precise == 'descending'",
to_either="subsequent_movement_precise == ['ascending', 'descending']",
to_leap="subsequent_movement == 'leap'",
to_same="subsequent_movement == 'same'",
last_notes="subsequent_movement == 'none'",
from_ascending="preceding_movement_precise == 'ascending'",
from_descending="preceding_movement_precise == 'descending'",
from_either="preceding_movement_precise == ['ascending', 'descending']",
from_leap="preceding_movement == 'leap'",
from_same="preceding_movement == 'same'",
first_notes="preceding_movement == 'none'",
to_and_from_ascending="subsequent_movement_precise == 'ascending' & preceding_movement_precise == 'ascending'",
to_and_from_descending="subsequent_movement_precise == 'descending' & preceding_movement_precise == 'descending'",
to_and_from_either="subsequent_movement_precise == ['ascending', 'descending'] & "
"preceding_movement_precise == ['ascending', 'descending']",
to_and_from_leap="subsequent_movement == 'leap' & preceding_movement == 'leap'",
to_and_from_same="subsequent_movement == 'same' & preceding_movement == 'same'",
)
regola_coverage = get_coverage_values(
"couperin", regola_vocabulary_major, regola_vocabulary_minor, **features
)
utils.print_heading(
"What percentage of each unigram category the RoO covers in Couperin"
)
regola_coverage
What percentage of each unigram category the RoO covers in Couperin
-------------------------------------------------------------------
mode coverage_of
major all 0.671580
diatonic 0.682466
to_ascending 0.806313
to_descending 0.739635
to_either 0.775385
to_leap 0.665190
to_same 0.403084
last_notes 0.827957
from_ascending 0.698709
from_descending 0.792703
from_either 0.742308
from_leap 0.680639
from_same 0.482379
first_notes 0.755474
to_and_from_ascending 0.847561
to_and_from_descending 0.857143
to_and_from_either 0.851648
to_and_from_leap 0.640768
to_and_from_same 0.444444
minor all 0.626947
diatonic 0.638242
to_ascending 0.765468
to_descending 0.714504
to_either 0.740741
to_leap 0.643806
to_same 0.363229
last_notes 0.629371
from_ascending 0.705036
from_descending 0.714504
from_either 0.709630
from_leap 0.643243
from_same 0.461883
first_notes 0.684783
to_and_from_ascending 0.811321
to_and_from_descending 0.745174
to_and_from_either 0.783835
to_and_from_leap 0.620690
to_and_from_same 0.409091
Name: proportion, dtype: float64
regola_coverage_cor = get_coverage_values(
"corelli", regola_vocabulary_major, regola_vocabulary_minor, **features
)
utils.print_heading(
"What percentage of each unigram category the RoO covers in Corelli"
)
regola_coverage_cor
What percentage of each unigram category the RoO covers in Corelli
------------------------------------------------------------------
mode coverage_of
major all 0.493474
diatonic 0.501944
to_ascending 0.560044
to_descending 0.536246
to_either 0.551528
to_leap 0.555137
to_same 0.210605
last_notes 0.784884
from_ascending 0.374654
from_descending 0.475670
from_either 0.410803
from_leap 0.592748
from_same 0.459298
first_notes 0.694362
to_and_from_ascending 0.457490
to_and_from_descending 0.559172
to_and_from_either 0.536245
to_and_from_leap 0.572093
to_and_from_same 0.098214
minor all 0.454785
diatonic 0.462010
to_ascending 0.403591
to_descending 0.410804
to_either 0.406355
to_leap 0.572739
to_same 0.197635
last_notes 0.871935
from_ascending 0.489461
from_descending 0.452261
from_either 0.475205
from_leap 0.532681
from_same 0.429054
first_notes 0.601108
to_and_from_ascending 0.336667
to_and_from_descending 0.572034
to_and_from_either 0.454955
to_and_from_leap 0.544018
to_and_from_same 0.142857
Name: proportion, dtype: float64
Comparing the regola against all “top k” vocabularies#
Campion’s regola comprises 10 different chords for both major and minor. For comparison, its values are shown at point 10.5 on the x-axis. The lower two plots show how many unigrams are covered by individual chords. Hover over the points to see the corresponding chords.
Show helpers
def make_coverage_plot_data(
bn_name, include_singular_vocabularies=True, **features
) -> pd.DataFrame:
BN = name2BN[bn_name]
all_chords = BN[["bass_degree", "intervals_over_bass"]].apply(tuple, axis=1)
chord_ranking = all_chords.groupby("mode").value_counts(normalize=True)
major_ranking, minor_ranking = (
chord_ranking.loc["major"],
chord_ranking.loc["minor"],
)
major_vocab, minor_vocab = [], []
results = {}
for i, (maj_chord, min_chord) in enumerate(
itertools.zip_longest(major_ranking.index, minor_ranking.index), 1
):
if maj_chord:
major_vocab.append(maj_chord)
if min_chord:
minor_vocab.append(min_chord)
key = ("cumulative", i) if include_singular_vocabularies else i
values = get_coverage_values(
bn_name, tuple(major_vocab), tuple(minor_vocab), **features
)
chord = pd.Series(str(maj_chord), index=values.index, name="chord")
chord.loc["minor"] = str(min_chord)
results[key] = pd.concat([values, chord], axis=1)
if not include_singular_vocabularies:
continue
single_maj_vocab = (maj_chord,) if maj_chord else None
single_min_vocab = (min_chord,) if min_chord else None
values = get_coverage_values(
bn_name, single_maj_vocab, single_min_vocab, **features
)
results[("single", i)] = pd.concat([values, chord], axis=1)
index_levels = ["vocabulary", "rank"] if include_singular_vocabularies else ["rank"]
return pd.concat(results, names=index_levels)
Show source
def plot_regola_vs_top_k_coverage(bn_name):
result = make_coverage_plot_data(bn_name, **features)
regola_results = pd.concat(
{("cumulative", 10.5): regola_coverage}, names=["vocabulary", "rank"]
).to_frame()
regola_results.loc[:, "chord"] = "regola"
result = pd.concat(
[
regola_results,
result,
]
).sort_index()
fig = px.line(
result.reset_index(),
x="rank",
y="proportion",
color="coverage_of",
facet_col="mode",
facet_row="vocabulary",
hover_name="chord",
log_x=True,
title=f"How many {bn_name.title()} unigrams are covered by each top-k vocabulary",
)
style_plotly(
fig,
match_facet_yaxes=True,
height=1500,
legend=dict(
orientation="h",
),
)
plot_regola_vs_top_k_coverage("couperin")
In order to inspect these plots you will want to hide traces. Click on a legend item to toggle it, double-click on an item to toggle all others.
plot_regola_vs_top_k_coverage("corelli")
In order to inspect these plots you will want to hide traces. Click on a legend item to toggle it, double-click on an item to toggle all others.